from functools import partial
import types
from schemas import *

#===============================================================================
#                     Property Factory class decorator
#===============================================================================
def json_properties(schema, mode='rwd', del_transforms=True, error_check=1):
    """
    Modify class 'cls' to have the property accessors defined in a schema. 
    The JSON data should be in the x._root attribute for instances of the given 
    class. 
    
    Usage
    -----
    
        json_properties(schema, [mode][, deltransforms])(class)
    
    Arguments
    ---------
    
    @param cls: (type) class to be modified.
    
    @param schema: (Schema) schema that validates a JSON structurea. 
    
    @param mode: (str) a string setting (r)ead, (w)rite and (d)elete support.
    
    @param del_transforms: (bool) if True, delete the from_* and to_* 
        trasformation functions.
    
    @error_check: (int) the level of error checking
        0 - no validation. 
        1 - basic validation when keys are accessed/changed/deleted. (default)
        2 - full validation. The dictionary is always valid if the user do not
            modify the dictionary directly.
        3 - paranoid. Performs a validation at every step. (for debugging purposes)
    
    Observations
    ------------
    
    If the class has methods named as '_<key>_in' and '_<key>_out', 
    they will be used as input/output transformations before saving/retrieving
    a given key in the JSON dictionary.
    
    Example 1 - simple read/write access
    ------------------------------------
    
    >>> schema = Schema({ u'name': Str(u''), u'gender': Str(u'') })
    >>> @json_properties(schema)
    ... class Foo(object): 
    ...     pass
    >>> 
    >>> x = Foo()
    >>> (x.name, x.gender)
    (u'', u'')
    >>> x.name = u'Bar'
    >>> x.json_raw
    {u'name': u'Bar'}
    
    Example 2 - function transformations
    ------------------------------------
    
    >>> from datetime import datetime
    >>> 
    >>> schema = Schema({ u'date': { u'day': Int(), u'month': Int(), u'year': Int() }})
    >>> 
    >>> @json_properties(schema)
    ... class Foo(object):
    ...    def __init__(self, date):
    ...        self.date = date
    ...    def T_date_in(self, d):
    ...        return { 'year': d.year, 'month': d.month, 'day': d.day }
    ...    def T_date_out(self, x):
    ...        return datetime(x['year'], x['month'], x['day'])
    >>> 
    >>> y = Foo(datetime(1982, 01, 06))
    >>> y.json_raw
    {u'date': {'month': 1, 'day': 6, 'year': 1982}}
    >>> y.json
    {u'date': datetime.datetime(1982, 1, 6, 0, 0)}
    """
    schema = Schema.as_schema(schema)

    # read/write/delete support
    mode_s = set(mode)
    read = 'r' in mode_s
    write = 'w' in mode_s
    delete = 'd' in mode_s
    if mode_s - set('rwd'):
        raise ValueError('invalid mode, %s' % repr(mode))

    return partial(json_properties_worker, schema, (read, write, delete), del_transforms, error_check)

def json_properties_worker(schema, (read, write, delete), del_transforms, error_check, cls):
    properties = { 'json_raw': property(get_json_raw),
                   '_json_': get_json_raw,
                   'json': property(partial(get_json, schema)),
                   'schema': schema }

    # define all properties from schema
    for prop, sch in schema.items():
        fget = fset = fdel = None

        if read:
            method = getattr(cls, 'T_%s_out' % prop, identity_method)
            fget = partial(fget_method, prop, sch, method)
        if write:
            method = getattr(cls, 'T_%s_in' % prop, identity_method)
            fset = partial(fset_method, prop, sch, method)
        if delete:
            fdel = partial(fdel_method, prop)

        properties[prop] = property(fget, fset, fdel)

    # save properties in class
    for p, v in properties.items():
        setattr(cls, p, v)

    # del transformation methods on request
    if del_transforms:
        for k in schema:
            if hasattr(cls, 'T_%s_in' % k):
                delattr(cls, 'T_%s_in' % k)
            if hasattr(cls, 'T_%s_out' % k):
                delattr(cls, 'T_%s_out' % k)

    return cls

# auxiliary getter functions ---------------------------------------------------
def identity_method(self, x):
    return x

def get_json_raw(self):
    try:
        return self._json
    except AttributeError:
        self._json = {}
        return self._json

def get_json(sch, self):
    items = ((k, getattr(self, k, None)) for k in sch)
    return dict((k, v) for (k, v) in items if v is not None)

def fget_method(prop, sch, func, self):
    json = getattr(self, '_json', {})
    obj = json.get(prop, getattr(sch, 'default', None))
    return func(self, obj)

def fset_method(prop, sch, func, self, value):
    value = func(self, value)
    try:
        sch.validate(value)
    except ValidationError, ex:
        raise ValidationError(('(%s):' % prop) + str(ex))
    if hasattr(sch, 'default') and value == sch.default:
        return
    else:
        try:
            root = self._json
        except AttributeError:
            root = self._json = {}
        root[prop] = value

def fdel_method(prop, self):
    try:
        root = self._json
    except AttributeError:
        return
    del root[prop]

__all__ = [ 'json_properties' ]

if __name__ == '__main__':
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)
